﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Text;
using System.Web;
using TallComponents.Web.Pdf.Resources;
using TallComponents.Web.Storage;
using TallComponents.PDF.TextExtraction;

namespace TallComponents.Web.Pdf
{
   /// <summary>
   /// Handles searching in PDF documents.
   /// </summary>
   public class SearchHandler : IHttpHandler, System.Web.SessionState.IReadOnlySessionState
   {
      private readonly IPdfStorage _storage;

      /// <summary>
      /// Initializes a new instance of the <see cref="SearchHandler"/> class.
      /// </summary>
      public SearchHandler()
         : this(PdfStorage.Provider)
      {

      }

      /// <summary>
      /// Initializes a new instance of the <see cref="SearchHandler"/> class.
      /// </summary>
      /// <param name="storage">PDF storage to use.</param>
      public SearchHandler(IPdfStorage storage)
      {
         if (storage == null)
         {
            throw new ArgumentNullException("storage");
         }
         _storage = storage;
      }

      /// <summary>
      /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
      /// </summary>
      /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
      public void ProcessRequest(HttpContext context)
      {
         if ("Post".Equals(context.Request.HttpMethod, StringComparison.OrdinalIgnoreCase))
         {
            // parse request parameters
            string text = context.Request.Params["find"];
            string storageKey = context.Request.Params["doc"];
            int searchedPagesCount = GetIntFromParam(context.Request.Params["searchedPages"]);
            int currentPageIndex = GetIntFromParam(context.Request.Params["currentPage"]);
            int startSearchPageIndex = GetIntFromParam(context.Request.Params["startPage"]);
            bool matchCase = GetBoolFromParam(context.Request.Params["matchCase"]);
            bool matchWholeWord = GetBoolFromParam(context.Request.Params["matchWholeWord"]);
            bool isRestarted = false;

            context.Response.ContentType = "application/json";

            if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(storageKey))
            {
               // empty query or no document specified.
               // return empty result set.
               context.Response.Write("[]");
               context.Response.End();
               return;
            }

            var document = _storage.GetDocument(storageKey);
            if (document != null && document.Pages.Count > currentPageIndex)
            {
               bool success = false;
               int pageIndex = 0;
               int numberOfPages = 0;
               int offset = 0;
               List<TextMatch> result = null;

               //perform a search unless we succeeded or pass through the all document
               while (!success && numberOfPages < document.Pages.Count)
               {
                  // start at startPage
                  pageIndex = numberOfPages + currentPageIndex - offset;
                  // wrap around to beginning if needed
                  if (pageIndex == document.Pages.Count - 1)
                  {
                     offset = document.Pages.Count;
                  }

                  result = TrySearchOnPage(context, storageKey, pageIndex, text, matchCase, matchWholeWord, isRestarted);
                  success = result != null && result.Count > 0;

                  if (pageIndex == startSearchPageIndex && searchedPagesCount > 0)
                  {
                     //that means that we returned to the page from which the search was started
                     //and we have searched through the all document
                     success = false;
                     context.Response.Write(SearchEndAsJson());
                     break;
                  }

                  if (!success)
                     numberOfPages++;
               }

               if (success)
               {
                  if (!context.Response.IsClientConnected)
                     context.Response.End();

                  if (null != result && result.Count > 0)
                     context.Response.Write(SearchResultsAsJson(result, isRestarted));
               }
            }
         }
         else
         {
            throw new HttpException(404, "Not found");
         }
      }

      List<TextMatch> TrySearchOnPage(HttpContext context, string storageKey, int pageIndex, string text, bool matchCase, bool matchWholeWord, bool isRestarted)
      {
         List<TextMatch> result = null;
         var page = _storage.GetPage(storageKey, pageIndex);
         if (null != page)
         {
            foreach (var match in page.Find(text, matchCase, matchWholeWord))
            {
               if (null == result)
               {
                  result = new List<TextMatch>();
               }
               result.Add(match);
            }
         }
         return result;
      }

      /// <summary>
      /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
      /// </summary>
      /// <returns>true</returns>
      bool IHttpHandler.IsReusable
      {
         get
         {
            return true;
         }
      }

      /// <summary>
      /// Gets an integer from a request parameter.
      /// </summary>
      /// <param name="param">The param.</param>
      /// <returns>The integer value of the parameter or 0 if not found.</returns>
      private static int GetIntFromParam(string param)
      {
         int result = 0;
         if (!string.IsNullOrEmpty(param))
         {
            if (param.EndsWith("px"))
               param = param.Substring(0, param.Length - 2);

            if (!int.TryParse(param.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out result))
            {
               double doubleValue;
               if (double.TryParse(param.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out doubleValue))
               {
                  result = Convert.ToInt32(doubleValue);
               }
            }
         }
         return result;
      }

      /// <summary>
      /// Gets the int from param.
      /// </summary>
      /// <param name="param">The param.</param>
      /// <returns>The boolean value of the parameter or false if not found.</returns>
      private static bool GetBoolFromParam(string param)
      {
         var result = false;
         if (!string.IsNullOrEmpty(param))
         {
            result = param.Trim().Equals("true", StringComparison.OrdinalIgnoreCase);
         }
         return result;
      }

      /// <summary>
      /// Get the JSON representation of a set of search results.
      /// </summary>
      /// <returns>
      /// The JSON representation of a set of search results.
      /// </returns>
      private static string SearchEndAsJson()
      {
         var sb = new StringBuilder();
         sb.Append("[");
         {
            sb.Append("{");
            sb.AppendFormat("Restarted:{0}", 1);
            sb.Append("}");
         }
         sb.Append("]");
         return sb.ToString();
      }


      /// <summary>
      /// Get the JSON representation of a set of search results.
      /// </summary>
      /// <param name="matches">The matches.</param>
      /// <param name="isRestarted">if set to <c>true</c> [is restarted].</param>
      /// <returns>
      /// The JSON representation of a set of search results.
      /// </returns>
      private static string SearchResultsAsJson(IEnumerable<TextMatch> matches, bool isRestarted)
      {
         if (matches == null)
         {
            throw new ArgumentNullException("matches");
         }

         var sb = new StringBuilder();
         sb.Append("[");
         bool first = true;
         foreach (var item in matches)
         {
            if (first)
            {
               first = false;
            }
            else
            {
               sb.Append(",");
            }
            sb.Append("{");
            sb.AppendFormat("Page:{0},Restarted:{1},Rectangles:", item.Page.Index, isRestarted ? 1 : 0);
            SerializeRectangles(sb, item);
            sb.Append("}");
         }
         sb.Append("]");
         return sb.ToString();
      }

      private static void SerializeRectangles(StringBuilder sb, TextMatch match)
      {
         if (match == null)
         {
            throw new ArgumentNullException("match");
         }

         // apply displacement for non-standard media box
         RectDisplacement transform = null;
         var mediaBox = match.Page.MediaBox;
         if (mediaBox.Bottom != 0 || mediaBox.Left != 0)
         {
            transform = new RectDisplacement
                           {
                              Left = -1 * mediaBox.Left,
                              Right = -1 * mediaBox.Left,
                              Bottom = -1 * mediaBox.Bottom,
                              Top = -1 * mediaBox.Bottom,
                           };
         }
         // apply displacement for cropbox
         var cropBox = match.Page.CropBox;
         if (null != cropBox && (
            mediaBox.Width != cropBox.Width ||
            mediaBox.Height != cropBox.Height ||
            mediaBox.Left != cropBox.Left ||
            mediaBox.Bottom != cropBox.Bottom))
         {
            transform = new RectDisplacement
                           {
                              Left = -1 * (mediaBox.Left + cropBox.Left),
                              Right = -1 * (mediaBox.Left + cropBox.Left),
                              Bottom = -1 * (mediaBox.Bottom + cropBox.Bottom),
                              Top = -1 * (mediaBox.Bottom + cropBox.Bottom),
                           };
         }

         bool first = true;
         sb.Append("[");

         foreach (SelectionRectangle rect in MergeGlyphRectangles(match.Glyphs))
         {
            if (null != transform)
            {
               // do transform
               rect.Bottom += transform.Bottom;
               rect.Top += transform.Top;
               rect.Left += transform.Left;
               rect.Right += transform.Right;
            }
            if (first)
            {
               first = false;
            }
            else
            {
               sb.Append(",");
            }
            rect.AppendAsJSON(sb);
         }
         sb.Append("]");
      }

      private class RectDisplacement
      {
         public double Left { get; set; }
         public double Bottom { get; set; }
         public double Top { get; set; }
         public double Right { get; set; }
      }

      /// <summary>
      /// Merges adjacent glyphs into selection rectangles.
      /// </summary>
      /// <remarks>This method will merge glyphs into selection rectangles. If glyphs are spread over multiple lines
      /// multiple selection rectangles are returned.</remarks>
      /// <param name="glyphs">The glyphs.</param>
      /// <returns>A set of selection rectangles.</returns>
      private static IEnumerable<SelectionRectangle> MergeGlyphRectangles(IEnumerable<Glyph> glyphs)
      {
         if (glyphs == null)
         {
            throw new ArgumentNullException("glyphs");
         }

         SelectionRectangle currentSelection = null;
         foreach (var glyph in glyphs)
         {
            var rect = RectFromPoints(Points(glyph));
            if (null == currentSelection)
            {
               currentSelection = rect;
            }
            else
            {
               if (rect.IsAdjacent(currentSelection, glyph.FontSize * 0.1))
               {
                  // extend current block
                  currentSelection = currentSelection.Union(rect);
               }
               else
               {
                  // return this block and start a new block
                  yield return currentSelection;
                  currentSelection = rect;
               }
            }
         }

         if (null != currentSelection)
         {
            yield return currentSelection;
         }
      }

      /// <summary>
      /// Enumerates the points that define the glyph bounding rectangle.
      /// </summary>
      /// <param name="glyph">The glyph.</param>
      /// <returns>An enumeration of the points that define the glyph bounding rectangle. </returns>
      private static IEnumerable<PointF> Points(Glyph glyph)
      {
         yield return glyph.BottomLeft;
         yield return glyph.BottomRight;
         yield return glyph.TopLeft;
         yield return glyph.TopRight;
      }

      /// <summary>
      /// Compares a set of points and returns a rectangle that envelopes all points.
      /// </summary>
      /// <param name="points">The points.</param>
      /// <exception cref="ArgumentException"></exception>
      /// <returns>A rectangle that envelopes all points.</returns>
      private static SelectionRectangle RectFromPoints(IEnumerable<PointF> points)
      {
         if (points == null)
         {
            throw new ArgumentNullException("points");
         }

         float xMin = 0.0F;
         float yMin = 0.0F;
         float xMax = 0.0F;
         float yMax = 0.0F;
         bool hasValue = false;

         foreach (var point in points)
         {
            if (!hasValue)
            {
               xMin = xMax = point.X;
               yMin = yMax = point.Y;
               hasValue = true;
            }
            else
            {
               xMin = (double.IsNaN(xMin) || point.X < xMin) ? point.X : xMin;
               yMin = (double.IsNaN(yMin) || point.Y < yMin) ? point.Y : yMin;
               xMax = (double.IsNaN(xMax) || point.X > xMax) ? point.X : xMax;
               yMax = (double.IsNaN(yMax) || point.Y > yMax) ? point.Y : yMax;
            }
         }

         if (!hasValue)
         {
            throw new ArgumentException(Text.No_values_in_collection, "points");
         }

         return new SelectionRectangle { Top = yMax, Left = xMin, Right = xMax, Bottom = yMin };
      }
   }
}